/******************************************************************************/

package jamie.org.sil.www.UsxVernacularCreator;

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Iterator;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.*;

import org.w3c.dom.*;


/******************************************************************************/
/**
 * Generic XML file handler.
 * <p>
 * 
 * This class is concerned with reading and writing XML files. We kind of
 * assume that the content of the files squares with the tags which the GUI
 * tool displays.  If it doesn't, then on input we ignore any tags which aren't
 * needed by the GUI, and on output we write out only those tags supported by
 * the GUI.
 * <p>
 * 
 * The class supports two different kinds of "tags".  One is identified by
 * a simple tag name, and is used for one-of-a-kind data.  The other is
 * identified by a combination of tag name and attributes, and is used for
 * types of data (such as book names) which may appear multiple times.
 */

public class XmlFile
{
  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                               Public                                   **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  public final static String C_LineSeparator = System.getProperty("line.separator");


  /****************************************************************************/
  /**
   * Returns the name of the file we are currently processing.
   * @return Filename
   */
  public String getFileName () { return m_FileName; }


  /****************************************************************************/
  /** Returns details of any validation errors.

      @return Details.
  */

  public String getValidationErrors () { return m_ValidationErrors; }


  /****************************************************************************/
  /** Get a particular item from an input XML file.

      @param path  XML path for item in question.

      @param dflt Default value used if the XML file provides no value.

      @return Corresponding value.
   */

  public Object getValue (String tagName, Object dflt)
  {
     int i = tagName.indexOf(" ");
     if (-1 == i)
       return getValueBasedOnTagNameOnly("//" + tagName, dflt);
     else
       try
       {
         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
         factory.setNamespaceAware(true);
         DocumentBuilder builder = factory.newDocumentBuilder();
         Document d = builder.parse(new ByteArrayInputStream(("<" + tagName + " />").getBytes("UTF-8")));
         d.getDocumentElement().normalize();
         return getValueBasedOnTagNameAndAttributes(d, "//" + tagName.substring(0, i), dflt);
       }
       catch (Exception e)
       {
         return dflt;
       }
   }


  /****************************************************************************/
  /** Populates all tags from a given file.

     @param fileName File containing data to be loaded.

     @throws IOException
     @throws javax.xml.parsers.ParserConfigurationException
     @throws org.xml.sax.SAXException
   */

  public void load (String fileName) throws IOException,
                                     javax.xml.parsers.ParserConfigurationException,
                                     org.xml.sax.SAXException
  {
    loadFile(fileName);
    XmlDetails.instance().populate(this);
  }


  /****************************************************************************/
  /** Validates data.

      @return Null if data is valid.  If data is not valid, the return value is
         the first tag found to have problems.
  */

  public ConfigTag.Generic validate ()
  {
    XmlDetails.iterator it = XmlDetails.instance().getIterator();
    ConfigTag.Generic res = null;
    m_ValidationErrors = "";

    while (it.hasNext())
    {
      ConfigTag.Generic tag = it.next();
      if (tag.isMissing())
      {
        if (null == res) res = tag;
        m_ValidationErrors = m_ValidationErrors + tag.getPrompt() + C_LineSeparator;
      }
    }

    if (0 != m_ValidationErrors.length())
      m_ValidationErrors =
        "Following items are mandatory, but have not been supplied ..." +
        C_LineSeparator +
        m_ValidationErrors.substring(0, m_ValidationErrors.length() - C_LineSeparator.length());

    return res;
  }


  /****************************************************************************/
  /** Writes the current data out to the current file.

      @throws IOException
   */

  public void write () throws IOException
  {
    write(m_FileName);
  }


  /****************************************************************************/
  /** Write the current data out.

      @param fileName

      @throws IOException
   */

  public void write (String fileName) throws IOException
  {
    boolean firstPrefix = true;
    String leader = "  ";
    BufferedWriter w =
      new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), "UTF-8"));
    
    w.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); w.newLine();
    w.write("<!-- Last updated ");
    w.write(new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z").format(Calendar.getInstance().getTime()));
    w.write(" -->");
    w.newLine();
    w.write("<" + XmlDetails.instance().getRootTag() + ">"); w.newLine();

    w.write(XmlDetails.instance().getHeader());

    XmlDetails.iterator it = XmlDetails.instance().getIterator();
    while (it.hasNext())
    {
      ConfigTag.Generic tag = it.next();
      String tagPrefix = tag.getPrefix();

      if (null != tagPrefix)
      {
        String[] lines = tagPrefix.split(C_LineSeparator);
        if (!firstPrefix) { w.newLine(); w.newLine(); w.newLine(); }
        firstPrefix = false;
        for (int i = 0; i < lines.length; ++i)
        { w.write(leader); w.write(lines[i]); w.newLine(); }
        leader += "  ";
      }

      String val = tag.getCanonicalValue().toString().trim();
      if (0 <= val.length())
      {
        String s = tag.getTagName();
        if (!s.startsWith("-"))
        {
          int p = s.indexOf(" ");
          if (p >= 0) s = s.substring(0, p);
          w.write(leader); w.write("<"); w.write(tag.getTagName()); w.write(">");
          w.write(val);
          w.write("</"); w.write(s); w.write(">"); w.newLine();
        }
      }

      if (null != tag.getSuffix())
      {
        String s = tag.getSuffix();
        leader = leader.substring(2);
        if (0 != s.length())
        {
          String[] lines = s.split(C_LineSeparator);
          for (int i = 0; i < lines.length; ++i)
          {
            w.write(leader);
            w.write(lines[i]);
            w.newLine();
          }
        }
      }
    }

    w.write(XmlDetails.instance().getTrailer());

    w.write("</" + XmlDetails.instance().getRootTag() + ">"); w.newLine();

    w.close();

    XmlDetails.instance().clearDirty();
    m_FileName = fileName;
  }





  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                              Private                                   **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /**************************************************************************/
  /* No, I don't understand this either, but I seem to need it to be able
     to find stuff in the dc namespace.

     See http://www.ibm.com/developerworks/library/x-javaxpathapi.html
  */

  public class MyNamespaceContext implements NamespaceContext
  {
    public String getNamespaceURI(String prefix)
    {
      if (prefix == null) throw new NullPointerException("Null prefix");
      else if ("dc".equals(prefix)) return "http://purl.org/dc/elements/1.1/";
      else if ("xml".equals(prefix)) return XMLConstants.XML_NS_URI;
      return XMLConstants.NULL_NS_URI;
    }

    // This method isn't necessary for XPath processing.
    public String getPrefix(String uri)
    {
      throw new UnsupportedOperationException();
    }

    // This method isn't necessary for XPath processing either.
    public Iterator getPrefixes(String uri)
    {
        throw new UnsupportedOperationException();
    }
  }


  /****************************************************************************/
  private Document m_DomDocument;
  private String m_FileName = "";
  private String m_ValidationErrors = "";
  private XPath m_Xpath = (XPathFactory.newInstance()).newXPath();


  /****************************************************************************/
  /* Gets a particular item from the XML file, where the item is identified
     by tagName and attributes jointly.

     @param d     XML document fragment which includes the attributes for this
                  lookup.

     @param path  XML path for item in question.

     @param dflt  Default value used if the config file provides no value.

     @return Corresponding value.
   */

  public Object getValueBasedOnTagNameAndAttributes (Document d, String path, Object dflt)
  {
    XPathExpression expr = null;
    try { expr = m_Xpath.compile(path); } catch(Exception e) { return dflt; }

    NamedNodeMap targetAttrs = null;
    try
    {
      targetAttrs = ((NodeList) expr.evaluate(d, XPathConstants.NODESET)).item(0).getAttributes();
    }
    catch (Exception e)
    {
    }

    try
    {
      NodeList nodeList = (NodeList) expr.evaluate(m_DomDocument, XPathConstants.NODESET);
      for (int i = 0; i < nodeList.getLength(); ++i)
      {
        Node n = nodeList.item(i);
        boolean matched = true;

        for (int j = 0; j < targetAttrs.getLength(); ++j)
        {
          Node targetAttr = targetAttrs.item(j);
          Node actualAttr = n.getAttributes().getNamedItem(targetAttr.getNodeName());
          if (null == actualAttr || !actualAttr.getNodeValue().equals(targetAttr.getNodeValue()))
          {
            matched = false;
            break;
          }
        }

        if (matched)
        {
	  String s = n.getFirstChild().getNodeValue();
          if (dflt instanceof Boolean)
          {
            s = s.trim(); if (0 == s.length()) s= "n";
            s = s.toLowerCase().substring(0, 1);
            return "t".equals(s) || "y".equals(s);
          }
          else
            return s;
        }
      }
    }
    catch(Exception e)
    {
    }

    return dflt;
  }


  /****************************************************************************/
  /** Gets a particular item from the XML file, where the item is identified
      by tagName only.

      @param path  XML path for item in question.

      @param dflt Default value used if the XML file provides no value.

      @return Corresponding value.
   */

  public Object getValueBasedOnTagNameOnly (String path, Object dflt)
  {
    XPathExpression expr = null;
    try { expr = m_Xpath.compile(path); } catch(Exception e) { return dflt; }

    try
    {
      NodeList nodeList = (NodeList) expr.evaluate(m_DomDocument, XPathConstants.NODESET);
      for (int i = 0; i < nodeList.getLength(); ++i)
        {
	  String s = nodeList.item(i).getFirstChild().getNodeValue();
          if (dflt instanceof Boolean)
          {
            s = s.trim(); if (0 == s.length()) s= "n";
            s = s.toLowerCase().substring(0, 1);
            boolean val = "t".equals(s) || "y".equals(s);
            return val;
          }
          else
            return s;
        }
    }
    catch(Exception e)
    {
    }

    return dflt;
  }


  /****************************************************************************/
  /**
     Loads an existing XML file.

     @param fileName

     @throws IOException
     @throws javax.xml.parsers.ParserConfigurationException
     @throws org.xml.sax.SAXException
  */

  private void loadFile (String fileName) throws IOException,
                                          javax.xml.parsers.ParserConfigurationException,
                                          org.xml.sax.SAXException
  {
    System.setProperty("javax.xml.parsers.DocumentBuilderFactory",
                       "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");

    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(true);
    DocumentBuilder builder = factory.newDocumentBuilder();
    m_DomDocument = builder.parse(new File(fileName));
    m_DomDocument.getDocumentElement().normalize();
    m_FileName = fileName;
    m_Xpath.setNamespaceContext(new MyNamespaceContext());
  }
}
